﻿using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Collections.Generic;
using System.ComponentModel;

namespace VideoReflector.Controls
{
    /// <summary>
    /// Custom panel for layout of child items horizontally within intervals along a line with their 
    /// position and width being determined by attached IntervalStart and IntervalEnd properties
    /// Potential uses include time-line and number-line layouts
    /// </summary>
    public class IntervalLinePanel : Panel
    {
        #region Dependency properties

        #region LowerBound

        /// <summary>
        /// Lower bound of layout line
        /// </summary>
        public double? LowerBound
        {
            get { return (double?)GetValue(LowerBoundProperty); }
            set { SetValue(LowerBoundProperty, value); }
        }
        
        /// <summary>
        /// LowerBound Dependency Property
        /// Backing store for LowerBound - enables animation, styling, binding etc.
        /// </summary>
        public static readonly DependencyProperty LowerBoundProperty =
            DependencyProperty.Register("LowerBound", typeof(double?), typeof(IntervalLinePanel),
            new FrameworkPropertyMetadata(null,
                    FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));

        public static double? GetLowerBound(DependencyObject d)
        {
            return (double?)d.GetValue(LowerBoundProperty);
        }

        public static void SetLowerBound(DependencyObject d, double? value)
        {
            d.SetValue(LowerBoundProperty, value);
        }

        #endregion // LowerBound

        #region UpperBound

        /// <summary>
        /// Upper bound of layout line
        /// </summary>
        public double? UpperBound
        {
            get { return (double?)GetValue(UpperBoundProperty); }
            set { SetValue(UpperBoundProperty, value); }
        }

        /// <summary>
        /// UpperBound Dependency Property
        /// Backing store for UpperBound - enables animation, styling, binding etc.
        /// </summary>
        public static readonly DependencyProperty UpperBoundProperty =
            DependencyProperty.Register("UpperBound", typeof(double?), typeof(IntervalLinePanel), 
                new FrameworkPropertyMetadata(null, 
                    FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));

        public static double? GetUpperBound(DependencyObject d)
        {
            return (double?)d.GetValue(UpperBoundProperty);
        }

        public static void SetUpperBound(DependencyObject d, double? value)
        {
            d.SetValue(UpperBoundProperty, value);
        }

        #endregion // UpperBound

        #endregion // Dependency properties

        #region Attached Dependency properties

        #region IntervalStart

        /// <summary>
        /// IntervalStart Attached Dependency Property
        /// Used to define start of interval child item is contained on in layout line
        /// </summary>
        public static readonly DependencyProperty IntervalStartProperty =
           DependencyProperty.RegisterAttached("IntervalStart", typeof(double?), typeof(IntervalLinePanel),
            new FrameworkPropertyMetadata(null,
                FrameworkPropertyMetadataOptions.AffectsParentMeasure | FrameworkPropertyMetadataOptions.AffectsParentArrange));

        public static double? GetIntervalStart(DependencyObject d)
        {
            return (double?)d.GetValue(IntervalStartProperty);
        }

        public static void SetIntervalStart(DependencyObject d, double? value)
        {
            d.SetValue(IntervalStartProperty, value);
        }

        #endregion // IntervalStart

        #region IntervalEnd

        /// <summary>
        /// IntervalEnd Attached Dependency Property
        /// Used to define end of interval child item is contained on in layout line
        /// </summary>
        public static readonly DependencyProperty IntervalEndProperty =
           DependencyProperty.RegisterAttached("IntervalEnd", typeof(double?), typeof(IntervalLinePanel),
            new FrameworkPropertyMetadata(null,
                FrameworkPropertyMetadataOptions.AffectsParentMeasure | FrameworkPropertyMetadataOptions.AffectsParentArrange));

        public static double? GetIntervalEnd(DependencyObject d)
        {
            return (double?)d.GetValue(IntervalEndProperty);
        }

        public static void SetIntervalEnd(DependencyObject d, double? value)
        {
            d.SetValue(IntervalEndProperty, value);
        }

        #endregion // IntervalEnd

        #endregion // Attached Dependency properties

        #region Layout overrides

        private int _requiredNumRows;

        /// <summary>
        /// Override for Measure pass of layout process
        /// Returns desired size of panel based on child element sizes - panel will try
        /// to be assigned enough space to allow each child to be given at least it's
        /// desired height width, based on layout of children in interval on a horizontal line
        /// with the width available to the child being a function of the proportion the associated interval
        /// makes up of the whole line and the total width of the panel. Overlapping intervals cause the associated
        /// child objects to be placed on separate rows so the desired height of a panel is a function of the
        /// minimum number of rows required to fit all child elements on the panel without overlapping, and the
        /// maximum height value of a child which is used as a common row height
        /// </summary>
        protected override Size MeasureOverride(Size availableSize)
        {
            var panelDesiredSize = new Size();

            // if bound properties not set (e.g. binding not updated yet) return dummy size
            if (UpperBound == null || LowerBound == null)
                return panelDesiredSize;

            double desiredWidth = 0;
            double desiredRowHeight = 0;
            
            // list of end positions of last intervals on each row
            var rowEnds = new List<double>();
            // add first row
            rowEnds.Add(0.0);

            // perform sort on children to order by interval start
            var sortedChildren = from UIElement child in Children
                                 orderby child.GetValue(IntervalStartProperty)
                                 select child;

            foreach (UIElement child in sortedChildren)
            {
                // get interval start / end properties
                double? childIntervalStart = (double?)child.GetValue(IntervalStartProperty);
                double? childIntervalEnd = (double?)child.GetValue(IntervalEndProperty);
                // only proceed if interval attached properties set
                if (childIntervalStart != null || childIntervalEnd != null)
                {
                    // use to calculate start and end positions for child element (0 far left, 1 far right)
                    // all casts safe as already checked if null
                    double startPos = ((double)childIntervalStart - (double)LowerBound) / 
                            ((double)UpperBound - (double)LowerBound);
                    double endPos = ((double)childIntervalEnd - (double)LowerBound) / 
                        ((double)UpperBound - (double)LowerBound);
                    // clip intervals to lie within bounds if necessary
                    startPos = startPos < 0 ? 0 : startPos > 1 ? 1 : startPos;
                    endPos = endPos < 0 ? 0 : endPos > 1 ? 1 : endPos;

                    // sanity checks
                    if (startPos >= endPos)
                    {
                        throw new Exception("IntervalStart property for IntervalLinePanel children must be greater than IntervalEnd property");
                    }
                    else if (endPos < 0 || startPos > 1)
                    {
                        throw new Exception("Child item intervals must lie at least partly within overall panel interval defined by LowerBound and UpperBound properties");
                    }

                    // find first row with end position less than or equal required start position
                    int row = rowEnds.FindIndex((end => end <= startPos));
                    // if no rows found (-1 index returned) add new row
                    if (row == -1)
                    {
                        row = rowEnds.Count();
                        rowEnds.Add(0.0);
                    }
                    // update row end position
                    rowEnds[row] = endPos;

                    // peform measurement pass on child (and sub-tree)
                    child.Measure(availableSize);
                    // desired row height is maximum of child desired heights
                    desiredRowHeight = child.DesiredSize.Height > desiredRowHeight ? child.DesiredSize.Height : desiredRowHeight;
                    // total panel width required to give child desired width is child's desired width divided by proportion
                    // child will take up of total line
                    double panelWidthForChild = child.DesiredSize.Width / (endPos - startPos);
                    // set desired width so that it meets the largest panel width required to at least meet the desired width
                    // for a child
                    desiredWidth = panelWidthForChild > desiredWidth ? panelWidthForChild : desiredWidth;
                }
            }

            _requiredNumRows = rowEnds.Count();

            panelDesiredSize.Width = desiredWidth;
            // TODO: adding RowSpacing property
            panelDesiredSize.Height = _requiredNumRows * desiredRowHeight;

            return panelDesiredSize;
        }

        /// <summary>
        /// Override for Arrange pass of layout process
        /// Arranges child objects within panel based on finalSize allocated to panel
        /// Follows layout alogrithm described in summary of MeasureOverride
        /// </summary>
        protected override Size ArrangeOverride(Size finalSize)
        {
            // if bound properties not set (e.g. binding not updated yet) return straight away
            if (UpperBound == null || LowerBound == null)
                return finalSize;

            var rowEnds = new List<double>();
            rowEnds.Add(0.0);

            // calculate rowHeight based on calculated required number of rows from Measure pass
            double rowHeight = finalSize.Height / _requiredNumRows;

            // perform sort on children to order by interval start
            var sortedChildren = from UIElement child in Children
                                 orderby child.GetValue(IntervalStartProperty)
                                 select child;

            foreach (UIElement child in sortedChildren)
            {
                // get interval start / end properties
                double? childIntervalStart = (double?)child.GetValue(IntervalStartProperty);
                double? childIntervalEnd = (double?)child.GetValue(IntervalEndProperty);
                if (childIntervalStart != null && childIntervalEnd != null)
                {
                    // use to calculate start and end positions for child element (0 far left, 1 far right)
                    // all casts safe as already checked if null
                    double startPos = ((double)childIntervalStart - (double)LowerBound) /
                            ((double)UpperBound - (double)LowerBound);
                    double endPos = ((double)childIntervalEnd - (double)LowerBound) /
                        ((double)UpperBound - (double)LowerBound);
                    // clip intervals to lie within bounds if necessary
                    startPos = startPos < 0 ? 0 : startPos > 1 ? 1 : startPos;
                    endPos = endPos < 0 ? 0 : endPos > 1 ? 1 : endPos;

                    // find first row with end position less than or equal to required start position
                    int row = rowEnds.FindIndex((end => end <= startPos));
                    // if no rows found (-1 index returned) add new row
                    if (row == -1)
                    {
                        row = rowEnds.Count();
                        rowEnds.Add(0.0);
                    }
                    // update row end position
                    rowEnds[row] = endPos;

                    // calculate position and size
                    double left = startPos * finalSize.Width;
                    double top = rowHeight * row;
                    double width = finalSize.Width * (endPos - startPos);
                    // peform Arrange pass on child (and sub-tree)
                    child.Arrange(new Rect(left, top, width, rowHeight));
                }
            }

            return finalSize;
        }

        #endregion // Layout overrides
    }
}
